using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Szmyd.Infiltrator.Autofac
{
    using System.Threading;

    public class ProfilingManager : IProfilingManager
    {
        private ConcurrentDictionary<string, ProfilingInfo> _calls = new ConcurrentDictionary<string, ProfilingInfo>(); 
        private readonly ThreadLocal<Stack<ProfilingInfo>> _stack = new ThreadLocal<Stack<ProfilingInfo>>(() => new Stack<ProfilingInfo>());
        private ThreadLocal<Guid> _threadId = new ThreadLocal<Guid>(Guid.NewGuid); 

        public bool IsStarted { get; set; }

        public void Start() {
            IsStarted = true;
        }

        public void Stop() {
            IsStarted = false;
        }

        public void Restart() {
            Stop();
            _calls = new ConcurrentDictionary<string, ProfilingInfo>();
            _stack.Value = new Stack<ProfilingInfo>();
            GC.Collect();
            Start();
        }

        public IEnumerable<ProfilingInfo> GetAll()
        {
            return _calls.Select(kv => kv.Value);
        }

        public IEnumerable<ProfilingInfo> GetForThread(Guid threadId)
        {
            return _calls.Select(kv => kv.Value).Where(v => v.ThreadId == threadId);
        }

        public void Register(InvocationData invocation)
        {
            var profilingInfo = _stack.Value.Pop();

            profilingInfo.TimeSpent = invocation.Duration;

            _calls.AddOrUpdate(profilingInfo.Id, profilingInfo, (s, info) =>
            {
                info.CallCount++;
                info.TimeSpent += profilingInfo.TimeSpent;
                return info;
            });
        }

        public void PushInvocation(InvocationData invocation)
        {

            var invocationInfo = new ProfilingInfo
            {
                TimeSpent = invocation.Duration,
                Method = invocation.Invocation.GetConcreteMethodInvocationTarget() ?? invocation.Invocation.GetConcreteMethod(),
                DeclaringType = invocation.Invocation.Method.DeclaringType,
                TargetType = invocation.Invocation.TargetType ?? invocation.Invocation.Method.DeclaringType,
                ParentId = null,
                ThreadId = _threadId.Value
            };

            invocationInfo.MethodName = string.Format("{2} {0}({1})", invocationInfo.Method.Name, string.Join(", ", invocation.Invocation.Method.GetParameters().Select(p => p.ParameterType)), invocationInfo.Method.ReturnType.Name);

            try
            {
                invocationInfo.ParentId = _stack.Value.Peek().Id;
            }
            catch (InvalidOperationException)
            {
                invocationInfo.ParentId = null;
            }

            var key = string.Format("{4}::{3}::{0}::{1}::{2}",
                invocation.Invocation.Method.DeclaringType.AssemblyQualifiedName,
                invocationInfo.TargetType.AssemblyQualifiedName,
                invocationInfo.MethodName,
                invocationInfo.ParentId,
                invocationInfo.ThreadId
               );

            invocationInfo.Id = key;

            _stack.Value.Push(invocationInfo);
        }

        public void PopInvocation()
        {
            try
            {
                _stack.Value.Pop();
            }
            catch (InvalidOperationException)
            {
            }
        }
    }
}